/**
 * Used to calculate the arithmetic lines.
 * 
 * @author Pavel Lagutik
 */
package org.ws.edu.task1.pool;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.ws.edu.task1.ICalculator;

public class CalcNum implements ICalculator {
    /** Line to calculate. */
    String str;

    static final String LINE_FACT = "([0-9]+)(\\.[0-9]+)?";
    static final String LINE_FACT_MINUS = "((\\-)?([0-9]+)(\\.[0-9]+)?)";
    static final Pattern PAT_FACT = Pattern.compile(LINE_FACT);
    static final Pattern PAT_MULT = Pattern.compile(LINE_FACT_MINUS + "[*]" + LINE_FACT_MINUS);
    static final Pattern PAT_DEV = Pattern.compile(LINE_FACT_MINUS + "[/]" + LINE_FACT_MINUS);
    static final Pattern PAT_PLUS = Pattern.compile(LINE_FACT_MINUS + "[+]" + LINE_FACT_MINUS);
    static final Pattern PAT_MINUS = Pattern.compile(LINE_FACT + "[-]" + LINE_FACT);
    static final Pattern PAT_ERECT = Pattern.compile(LINE_FACT_MINUS + "\\^" + LINE_FACT);
    static final Pattern PAT_ROOT = Pattern.compile(LINE_FACT + "[|]" + LINE_FACT);

    CalcNum() {
        str = new String();
    }

    /**
     * The method gets the string, starts testing methods and arithmetic calculations.
     * 
     * @param enterStr String typed by the user.
     * @return Returns the value or an error.
     */
    public String calculateLine(String enterStr) {
        removesSpaces(enterStr);
        if (isValue()) {
            return Math(0, 0);
        }
        return null;
    }

    /**
     * Removes spaces from the input string. Initializes the field {@link Item2d#str}.
     * 
     * @param str Returns the converted string.
     */
    public void removesSpaces(String str) {
        str = str.replaceAll(" +", "");
        this.str = str;
    }

    /**
     * Check string for correct characters.
     * 
     * @return true or false.
     */
    public boolean isValue() {
        char c;
        for (int i = 0; i < str.length(); i++) {
            c = str.charAt(i);
            if (c < '0' && c > '9' && c != '(' && c != ')' && c != '*' && c != '/' && c != '+' && c != '-' && c != '!' && c != '.'
                    && c != '^' && c != '|') {
                return false;
            }
        }
        return true;
    }

    /**
     * From string identifies simple arithmetic expressions in parentheses.
     * 
     * @param totalPos The position of the flag on the line.
     * @param bracket Flag the presence of parentheses.
     * @return Returns the result of a string or a substring.
     */
    public String Math(int totalPos, int bracket) {
        String strTemp = new String();
        String insertStr = new String();
        for (int i = totalPos; i < str.length(); i++, totalPos++) {
            if (str.charAt(i) == '(') {
                str = Math(totalPos + 1, bracket + 1);
            }
            if (bracket > 0) {
                while (str.charAt(i) != ')' && i < str.length() - 1) {
                    if (str.charAt(i) == '(') {
                        str = Math(i, bracket + 1);
                    }
                    strTemp += str.charAt(i);
                    i++;
                }
                insertStr = calculator(strTemp);
                str = replaceString(str, "(" + strTemp + ")", insertStr);
                return str;
            }
        }
        return isNumber(calculator(str));
    }

    /**
     * Verifies the correctness of the output.
     * 
     * @param strTemp The string to test.
     * @exception NumberFormatException If the result is not a number.
     * @return Returns a string or an error.
     */
    public String isNumber(String strTemp) {
        try {
            Double.parseDouble(strTemp);
            return strTemp;
        } catch (NumberFormatException e) {
            return null;
        }
    }

    /**
     * Checks a string for arithmetic.
     * 
     * @param result The line for the calculations.
     * @exception NumberFormatException Finding factorial in an incorrect number.
     * @return Returns the value or an error.
     */
    public String calculator(String result) {
        String resultTemp = new String();
        String[] ch = new String[3];
        Matcher matcher;

        matcher = PAT_ROOT.matcher(result);
        while (matcher.find()) {
            ch = (String.valueOf(matcher.group())).split("[|]");
            resultTemp = String.valueOf(takeRoot(Double.valueOf(ch[0]), Double.valueOf(ch[1])));
            result = replaceString(result, String.valueOf(ch[0] + '|' + ch[1]), resultTemp);
        }

        matcher = PAT_ERECT.matcher(result);
        while (matcher.find()) {
            ch = (String.valueOf(matcher.group())).split("\\^");
            resultTemp = String.valueOf(raisesToPower(Double.valueOf(ch[0]), Double.valueOf(ch[1])));
            result = replaceString(result, String.valueOf(ch[0] + '^' + ch[1]), resultTemp);
        }

        matcher = PAT_FACT.matcher(result);
        while (matcher.find()) {
            ch = (String.valueOf(matcher.group())).split("[!]");
            double dNum = Double.valueOf(ch[0]);
            try {
                resultTemp = String.valueOf((double) factorial((int) dNum));
            } catch (NumberFormatException e) {
                System.out.println("Factorial can not be a fraction or a negative number!");
                return null;
            }
            result = replaceString(result, String.valueOf(ch[0] + '!'), resultTemp);
        }

        char[] symbol = { '*', '/' };
        for (int i = 0; i < 2; i++) {
            for (int j = 0; j < result.length(); j++) {
                if (result.charAt(j) == symbol[0] || result.charAt(j) == symbol[1]) {
                    result = calcAction(result, result.charAt(j));
                    j = 1;
                }
            }
            symbol[0] = '+';
            symbol[1] = '-';
        }
        return result;
    }

    /**
     * Arithmetical calculation produces two numbers.
     * 
     * @param result The line for the calculations.
     * @param symb Symbol arithmetic calculations.
     * @exception Exception Division by zero.
     * @return Returns the value or an error.
     */
    public String calcAction(String result, char symb) {
        Matcher matcher;
        String resultTemp = new String();
        String[] ch = new String[3];

        switch (symb) {
            case '*':
                matcher = PAT_MULT.matcher(result);
                if (matcher.find()) {
                    ch = (matcher.group()).split("[*]");
                    resultTemp = String.valueOf(mul(Double.valueOf(ch[0]), Double.valueOf(ch[1])));
                    result = replaceString(result, String.valueOf(ch[0] + '*' + ch[1]), resultTemp);
                }
                break;
            case '/':
                matcher = PAT_DEV.matcher(result);
                if (matcher.find()) {
                    ch = (matcher.group()).split("[/]");
                    try {
                        resultTemp = String.valueOf(div(Double.valueOf(ch[0]), Double.valueOf(ch[1])));
                    } catch (Exception e) {
                        System.err.println("Division by Zero!!!");
                        return null;
                    }
                    if (resultTemp.equals("Infinity") || resultTemp.equals("-Infinity") || resultTemp.equals("NaN")) {
                        System.err.println("Division by Zero!!!");
                        return null;
                    } else {
                        result = replaceString(result, String.valueOf(ch[0] + '/' + ch[1]), resultTemp);
                    }
                }
                break;
            case '+':
                matcher = PAT_PLUS.matcher(result);
                if (matcher.find()) {
                    ch = (matcher.group()).split("[+]");
                    resultTemp = String.valueOf(sum(Double.valueOf(ch[0]), Double.valueOf(ch[1])));
                    result = replaceString(result, String.valueOf(ch[0] + '+' + ch[1]), resultTemp);
                }
                break;
            case '-':
                String znak = "";
                if (result.charAt(0) == '-')
                    znak = "-";
                matcher = PAT_MINUS.matcher(result);
                if (matcher.find()) {
                    ch = (matcher.group()).split("[-]");
                    resultTemp = String.valueOf(sub(Double.valueOf(znak + ch[0]), Double.valueOf(ch[1])));
                    result = replaceString(result, String.valueOf(znak + ch[0] + '-' + ch[1]), resultTemp);
                }
                znak = "";
                break;
            default:
                break;
        }
        return result;
    }

    /**
     * Replaces a substring of another string.
     * 
     * @param initLine Line over which the action is performed.
     * @param findLine String that should be changed.
     * @param replaceLine The string to be inserted.
     * @return Returns the total line.
     */
    protected String replaceString(String initLine, String findLine, String replaceLine) {
        if (initLine != null && initLine.length() > 0) {
            int a = 0;
            int b = 0;
            while (true) {
                a = initLine.indexOf(findLine, b);
                if (a != -1) {
                    initLine = initLine.substring(0, a) + replaceLine + initLine.substring(a + findLine.length());
                    b = a + replaceLine.length();
                } else
                    break;
            }
        }
        return initLine;
    }

    @Override
    public int sum(int a1, int a2) {
        return a1 + a2;
    }

    @Override
    public long factorial(int num) {
        return (num == 0) ? 1 : num * factorial(num - 1);
    }

    public double mul(double a1, double a2) {
        return a1 * a2;
    }

    public double div(double a1, double a2) {
        return a1 / a2;
    }

    public double sum(double a1, double a2) {
        return a1 + a2;
    }

    public double sub(double a1, double a2) {
        return a1 - a2;
    }

    /**
     * Method raises a number to a power.
     * 
     * @param num Number you want to raise to a power.
     * @param pow Power of the number.
     * @return Evaluation result.
     */
    public double raisesToPower(double num, double pow) {
        return Math.pow(num, pow);
    }

    /**
     * Extraction of the root of a number.
     * 
     * @param num Number from which to retrieve the root.
     * @param i The value of the root.
     * @return Evaluation result.
     */
    public double takeRoot(double num, double i) {
        return new BigDecimal(Math.pow(num, 1 / i)).setScale(3, RoundingMode.HALF_UP).doubleValue();
    }

}